#!/usr/bin/env python
#
# Grid/cluster job abstractions
#
# Author:   Mike Murphy <mamurph@cs.clemson.edu>
# Revision: 23 September 2009
#
#   Copyright 2009 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

'''
Components used for simulating jobs. These components include regular jobs,
which represent workloads sent to a grid system by an end user, and pilot jobs,
which are jobs that lease a remote system for the purpose of running a task
using overlay scheduling.

Apart from the relative CPU and network bounding of a job, the actual
workload contained within a job is irrelevant for simulation purposes. As long
as the length of a job is known (for regular jobs), the simulation of
scheduling and grid handling behavior is possible.

All regular jobs have a CPU/network bounding adjustment, which is a rough
estimate of the relative portion of a job that is either CPU-bound or
network-bound. Each bound is expressed as a numeric percentage in whole
numbers (e.g. 50 for 50%) such that the sum of the two values is always
100 (i.e. cpu + net = 100).

Jobs also have the capability to incur random errors with a given
probability (default 0). The source of randomness is the simulator random
source, which is normally a uniform random distribution.
'''

from core_common import PJ_START, PJ_STOP, PJ_KILL
from results import DiscrepancyResult, JobResult, JobStartResult, \
                    JobSubmitResult
#


class Job(object):
   '''
   Simulated representation of a regular job, which represents a workload
   submitted by the user to run on a remote system. It is assumed that
   a priori information about the job length is available, which is used to
   schedule job completion.
   '''
   def __init__(self, length, cpu=100, name='', vo='', prob_err=0.0, **kwds):
      '''
      @param length: Length of the job in nonnegative time units
      @type length: number
      @param cpu: CPU bound of the job (0-100)
      @type cpu: number
      @param name: Job ID or name of the job
      @type name: str
      @param vo: Name of the Virtual Organization affiliated with job
      @type vo: str
      @param prob_err: Probability of a job error (0.0-1.0)
      @type prob_err: float
      @param kwds: Job extra data
      '''
      self.extra_data = kwds
      self.length = length
      self.actual_length = length
      self.cpu = min(cpu, 100)
      self.net = 100 - self.cpu
      self.name = name
      self.vo = vo
      self.prob_error = prob_err
      self.running = False
      self.complete = False
      self.start_time = -1
      self.submit_time = -1
      self.finish_time = -1
      self.worker_node = None
      self.sched = None
      self.pending_evt = None
      self.error = False
   def submit(self, time, sched):
      '''
      Method called by the scheduler when submitting a job. Records job
      submission time and a reference to the scheduler.

      @param time: Time at which the job is submitted
      @type time: number
      @param sched: Reference to the scheduler invoking this method

      @rtype: L{results.JobSubmitResult}
      '''
      self.submit_time = time
      self.sched = sched
      return JobSubmitResult(time, self)
   def start(self, time, sim, node):
      '''
      Method called by the scheduler to start a job. Injects random errors if
      the error probability is greater than zero. Execution of this method
      schedules an event to call the finish() method of the same instance at
      the completion time (determined by the job length and any adjustment
      factor provided by the node on which the job execute).

      @param time: Job start time
      @type time: number
      @param sim: Reference to the simulator object
      @param node: Machine on which the job has been started

      @rtype: L{results.JobStartResult}
      '''
      self.running = True
      self.start_time = time
      self.worker_node = node
      r = []
      # Adjust job length for CPU/network bounding, based on the actual machine
      # to which the job has been mapped
      if node:
         self.actual_length = \
	    node.adjust_length(self.length, self.cpu, self.net)
         node.current_job = self
      # Inject random job error, if requested
      if self.prob_error > 0:
         p = sim.random.random()
         if p < self.prob_error:
            self.error = True
      length = self.actual_length
      if length < 0:
         reason = 'negative length: %d' % length
	 resln = 'setting length to 0 and marking job as error'
         length = 0
	 self.error = True
	 r.append(DiscrepancyResult(time, self.name, reason, resln))
      #
      if self.error:
         # Job has been killed, but need self.finish() to run so that a
         # JobResult gets created
         length = 0
      #
      self.set_finish(time + length, sim)
      #
      r.append(JobStartResult(time, self))
      return (r,)
   def set_finish(self, when, simobj):
      '''
      Sets or updates the time at which the job will finish. If the job is not
      running, then this method will have no effect.

      @param when: Time at which job will finish
      @type when: number
      @param simobj: Reference to the simulator object
      '''
      if self.running:
         if self.pending_evt is not None:
            simobj.cancel_event(self.pending_evt)
         self.pending_evt = simobj.new_simple_event(when, self.finish)
   def finish(self, time, simobj):
      '''
      Simulator-aware method that is called at job completion time by an event
      set by the start() method.

      @param time: Job completion time
      @type time: number
      @param simobj: Reference to the simulator object

      @return: result collection for simulator output
      '''
      self.pending_evt = None
      self.running = False
      self.complete = True
      self.finish_time = time
      if self.worker_node:
         self.worker_node.current_job = None
      sr = self.sched.job_done(self.worker_node, self)
      return ([sr, JobResult(time, self)],)
   def kill(self, time, simobj):
      '''
      Method invoked by the scheduler, worker node, or other component when
      this job is to be killed. The job error flag will be set to True. If
      the job is already running, the event to call the finish() method will
      be moved to the current time, and finish() will still be called so that
      a JobResult gets transmitted to the simulator. If the job is NOT yet
      running, it is up to the client code to ensure that a record of the
      job is added to the output.

      @param time: Time at which job is killed
      @type time: number
      @param simobj: Reference to the simulator object
      '''
      self.error = True
      self.actual_length = time - self.start_time
      self.set_finish(time, simobj)
   def __str__(self):
      desc  = '***** Job Record *****\n'
      desc += 'Job: %s\n' % str(self.name)
      if self.vo != '':
         desc += '   vo:  %s\n' % self.vo
      desc += '   submit time:  %d\n' % self.submit_time
      desc += '   started:  %d\n' % self.start_time
      desc += '   finished: %d\n' % self.finish_time
      desc += '   length:  %d\n' % self.length
      desc += '   running:  ' + str(self.running) + '\n'
      desc += '   complete:  ' + str(self.complete) + '\n'
      desc += '   success:  ' + str(not self.error) + '\n'
      for key in self.extra_data:
         desc += '   %s:  %s\n' % (str(key), str(self.extra_data[key]))
      if self.worker_node != None:
         desc += str(self.worker_node)
      desc += '**********'
      return desc
   #
   # Hack around Python lists calling repr instead of str
   def __repr__(self):
      return self.__str__()
#


class PilotJob(Job):
   '''
   A pilot job is a job that is submitted to the grid for the purpose of
   leasing a remote resource. Once obtained, the resource initiates a
   callback to an overlay scheduler or other component, in order to retrieve
   the workload to be executed.
   '''
   def __init__(self, callback, name='', vo='', pr_err=0.0, **kwds):
      '''
      @param callback: Callback function to invoke when the pilot job starts.
                       This function is invoked with 5 arguments: the start
                       time, a reference to the simulator, a reference to the
                       scheduler used to submit the pilot job, a reference to
                       the machine on which the pilot job is running, and a
                       state flag (see the flags in the L{core_common} module,
                       prefixed with PJ_).
      @param name: Name of the pilot job
      @type name: str
      @param vo: Name of the Virtual Organization associated with the pilot
                 job
      @type vo: str
      @param pr_err: Probability of pilot job error (range 0.0-1.0). If a
                     random error occurs in the pilot, the callback function
                     will NOT be invoked.
      @type pr_err: float
      @param kwds: Job extra data
      '''
      Job.__init__(self, length=-1, name=name, vo=vo, prob_err=pr_err, **kwds)
      self.callback = callback
      self.sim = None    # set in start
   def start(self, time, sim, node):
      '''
      Starts a pilot job. Since the length of the pilot job is not known a
      priori, the job will execute until the notify_done() method is called
      or the pilot job is killed. If the pilot job experiences a random error,
      it will terminate at the same clock time, without invoking the callback.

      @param time: Clock time at which job is started
      @type time: float
      @param sim: Reference to the simulator object
      @param node: Reference to the machine on which the job is to be started

      @return: result collection including any return value from the callback
               function and a L{results.JobStartResult}
      '''
      r = []
      self.sim = sim
      self.worker_node = node
      self.running = True
      self.start_time = time
      self.keep_alive = True
      if self.prob_error > 0:
         p = sim.random.random()
         if p < self.prob_error:
            self.error = True
      if not self.error:
         cb = self.callback(self, time, sim, self.sched, node, PJ_START)
         if cb is not None:
            r.append(cb)
      else:
         self.set_finish(time, sim)
      r.append(JobStartResult(time, self))
      return (r,)
   def notify_done(self, time, sim):
      '''
      Simulator-aware method used by the external process (the one utilizing
      the pilot job to lease resources) to notify the pilot job to end

      @param time: Current simulated time
      @type time: number
      @param sim: Reference to the simulator object
      '''
      self.length = time - self.start_time
      self.actual_length = self.length
      self.set_finish(time, sim)
   def terminate(self, signum=PJ_STOP):
      '''
      Notifies the external process (callback function) that the pilot job is
      being terminated (provided the job has started), then submits an event
      to terminate the pilot job immediately.

      @param signum: Pilot job signal number (L{core_common.PJ_STOP} or
                     L{core_common.PJ_KILL})

      @return: simulator results collection, which may be empty
      '''
      r = []
      if self.running:
         ts = self.sim.get_time()
         cb = self.callback(self, ts, self.sim, self.sched, \
                            self.worker_node, signum)
         if cb is not None:
            r.append(cb)
         self.notify_done(ts, self.sim)
      return (r,)
   def kill(self, time, simobj):
      '''
      Kills the pilot job, notifying the external process (callback function)
      of the kill, if it is running. A kill happens immediately, and the
      external process MUST terminate immediately, for accurate simulation
      (equivalent to a SIGKILL in UNIX). This method invokes the terminate()
      method with a signal number of PJ_KILL.

      @param time: Time at which pilot job is killed
      @type time: number
      @param simobj: Reference to the simulator object
      '''
      self.error = True
      self.terminate(signum=PJ_KILL)
#
